commonlibsse_ng\skse\interfaces/messaging.rs
1// C++ Original code
2// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/SKSE/Interfaces.h
3// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/SKSE/Interfaces.cpp
4// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
5// SPDX-License-Identifier: MIT
6//
7// SPDX-FileCopyrightText: (C) 2025 SARDONYX
8// SPDX-License-Identifier: Apache-2.0 OR MIT
9
10//! Rust bindings for the SKSE Messaging Interface
11//!
12//! This module provides Rust representations of the SKSE messaging system, which allows plugins to communicate with each other.
13//! It includes message types, dispatchers, and the main `MessagingInterface` wrapper.
14//!
15//! # References
16//! - [Original C++ Code](https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/SKSE/Interfaces.h)
17//! - [C++ Implementation](https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/SKSE/Interfaces.cpp)
18use crate::skse::{
19 api::{ApiStorageError, get_plugin_handle},
20 impls::stab::SKSEMessagingInterface,
21};
22use std::{
23 borrow::Cow,
24 ffi::{CStr, c_char, c_void},
25};
26
27/// Represents the different types of messages that can be sent or received through SKSE's messaging system.
28#[commonlibsse_ng_derive_internal::ffi_enum]
29#[repr(u32)]
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum MessageType {
32 /// Fired after all plugins are loaded.
33 PostLoad,
34 /// Fired after all `PostLoad` events have completed.
35 PostPostLoad,
36 /// Fired before loading a game save.
37 PreLoadGame,
38 /// Fired after loading a game save.
39 PostLoadGame,
40 /// Fired before saving a game.
41 SaveGame,
42 /// Fired before deleting a game save.
43 DeleteGame,
44 /// Fired when the input system is loaded.
45 InputLoaded,
46 /// Fired when starting a new game.
47 NewGame,
48 /// Fired after all game data has loaded.
49 DataLoaded,
50}
51
52/// Represents the different event dispatchers that SKSE provides.
53#[commonlibsse_ng_derive_internal::ffi_enum]
54#[repr(u32)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub enum Dispatcher {
57 /// Dispatcher for mod events.
58 ModEvent = 0,
59 /// Dispatcher for camera events.
60 CameraEvent,
61 /// Dispatcher for crosshair events.
62 CrosshairEvent,
63 /// Dispatcher for action events.
64 ActionEvent,
65 /// Dispatcher for NiNode update events.
66 NiNodeUpdateEvent,
67}
68
69/// Represents a message sent through the SKSE messaging system.
70#[derive(Clone)]
71#[repr(C)]
72pub struct Message {
73 /// The name of the sender as a C string.
74 pub sender: *const c_char,
75 /// The type of message.
76 pub msg_type: MessageType_CEnum,
77 /// The length of the data buffer.
78 pub data_len: u32,
79 /// Pointer to the message data.
80 ///
81 /// # Note
82 /// The pointer may be invalid, even if `data_len` is non-zero.
83 /// Always use [`Message::get_valid_data`] to safely access the data.
84 pub data: *mut c_void,
85}
86
87impl Message {
88 /// Returns a valid memory slice of the message data, if accessible.
89 ///
90 /// # Safety
91 /// - This function checks if the pointer is non-null and properly aligned.
92 /// - It also ensures that the memory range is valid before returning a reference.
93 ///
94 /// # When `None` is returned
95 /// - If `data_len` is 0.
96 /// - If the data pointer is null, unaligned, or invalid.
97 pub fn get_valid_data(&self) -> Option<&[u8]> {
98 use crate::rex::win32::is_valid_range;
99
100 if self.data_len == 0 {
101 return None;
102 }
103
104 let data = self.data.cast::<u8>();
105 if data.is_null() && !data.is_aligned() {
106 return None;
107 }
108
109 let len = self.data_len as usize;
110 is_valid_range(data, len).then_some(unsafe { core::slice::from_raw_parts(data, len) })
111 }
112}
113
114// # Why does this struct need to implement Debug manually?
115// In the case of `*const c_char`, Debug is a memory address, **which is difficult to debug**.
116// Therefore, implement it manually and display the string.
117impl core::fmt::Debug for Message {
118 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119 let data: Cow<'static, str> = if self.data_len == 0 {
120 "".into()
121 } else if !self.data.is_null() && self.data.cast::<u8>().is_aligned() {
122 if crate::rex::win32::is_valid_range(self.data.cast::<u8>(), self.data_len as usize) {
123 String::from_utf8_lossy(unsafe {
124 core::slice::from_raw_parts(self.data.cast::<u8>(), self.data_len as usize)
125 })
126 } else {
127 "inaccessible ptr".into()
128 }
129 } else {
130 "null/unaligned ptr".into()
131 };
132
133 f.debug_struct("Message")
134 .field("sender", &unsafe { CStr::from_ptr(self.sender) })
135 .field("msg_type", &self.msg_type)
136 .field("data_len", &self.data_len)
137 .field("data_ptr", &self.data)
138 .field("data", &data)
139 .finish()
140 }
141}
142
143/// APIs that enable data to be sent and received between plugins.
144#[derive(Debug, Clone)]
145#[repr(transparent)]
146pub struct MessagingInterface(&'static SKSEMessagingInterface);
147
148impl MessagingInterface {
149 /// The version number of the messaging interface.
150 pub const VERSION: u32 = 2;
151
152 /// Creates a new `MessagingInterface` instance from the raw SKSE interface.
153 #[inline]
154 pub(crate) const fn new(interface: &'static SKSEMessagingInterface) -> Self {
155 Self(interface)
156 }
157
158 /// Returns the version number of the messaging interface.
159 #[inline]
160 pub const fn version(&self) -> u32 {
161 self.0.interfaceVersion
162 }
163
164 /// Dispatches a message to SKSE listeners.
165 ///
166 /// # Errors
167 /// If the internal global API storage is uninitialized because forgot to call `skse::init`
168 #[inline]
169 pub fn dispatch<T>(
170 &self,
171 message_type: MessageType,
172 data: &mut T,
173 data_len: u32,
174 receiver: Option<&CStr>,
175 ) -> Result<(), MessagingError> {
176 unsafe { self.dispatch_raw(message_type, (data as *mut T).cast(), data_len, receiver) }
177 }
178
179 /// Dispatches a message to SKSE listeners.
180 ///
181 /// # Errors
182 /// If the internal global API storage is uninitialized because forgot to call `skse::init`
183 ///
184 /// # Safety
185 /// If the reference to the pointer pointing to data is valid.
186 pub unsafe fn dispatch_raw(
187 &self,
188 message_type: MessageType,
189 data: *mut c_void,
190 data_len: u32,
191 receiver: Option<&CStr>,
192 ) -> Result<(), MessagingError> {
193 let result = unsafe {
194 (self.0.Dispatch)(
195 get_plugin_handle()?,
196 message_type as u32,
197 data,
198 data_len,
199 receiver.map_or(core::ptr::null_mut(), |cstr| cstr.as_ptr()),
200 )
201 };
202 if !result {
203 return Err(MessagingError::DispatchFailed {
204 message_type,
205 receiver: receiver
206 .map_or("all listeners", |receiver| receiver.to_str().unwrap_or_default())
207 .to_string(),
208 });
209 }
210
211 Ok(())
212 }
213
214 /// Gets the event dispatcher for a specific dispatcher id.
215 #[inline]
216 pub fn get_event_dispatcher(&self, dispatcher_id: Dispatcher) -> *mut c_void {
217 unsafe { (self.0.GetEventDispatcher)(dispatcher_id as u32) }
218 }
219
220 /// Registers a listener for SKSE's in-game events (e.g., loading saves).
221 ///
222 /// # Errors
223 /// If the internal global API storage is uninitialized because forgot to call `skse::init`
224 ///
225 /// # Event Data
226 /// - `PreLoadGame`: The name of the save data
227 /// - `PostLoadGame`: Invalid ptr(data length 1)
228 ///
229 /// # Example
230 ///
231 /// ```rust
232 /// if let Ok(messaging) = commonlibsse_ng::skse::api::get_messaging_interface() {
233 /// messaging.register_skse_listener(|message| {
234 /// #[cfg(feature = "tracing")]
235 /// tracing::info!("SKSE event: {message:#?}");
236 /// });
237 /// }
238 /// ```
239 #[inline]
240 pub fn register_skse_listener(&self, f: fn(msg: &Message)) -> Result<(), MessagingError> {
241 self.register_listener(c"SKSE", f)
242 }
243
244 /// Registers a listener for a specific plugin's in-game events.
245 ///
246 /// # Errors
247 /// If the internal global API storage is uninitialized because forgot to call `skse::init`
248 pub fn register_listener(
249 &self,
250 sender: &CStr,
251 f: fn(msg: &Message),
252 ) -> Result<(), MessagingError> {
253 #[allow(clippy::fn_to_numeric_cast_any)]
254 let void_callback = (f as *mut fn(msg: &Message)).cast::<c_void>();
255 let result = unsafe {
256 (self.0.RegisterListener)(get_plugin_handle()?, sender.as_ptr(), void_callback)
257 };
258
259 if !result {
260 return Err(MessagingError::RegisterListenerFailed {
261 sender_name: sender.to_string_lossy().to_string(),
262 });
263 }
264
265 Ok(())
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, snafu::Snafu)]
270pub enum MessagingError {
271 /// Failed to dispatch message to {receiver}, kind: {message_type:?}
272 DispatchFailed { message_type: MessageType, receiver: String },
273
274 /// Failed to register listener for sender: {sender_name}
275 RegisterListenerFailed { sender_name: String },
276
277 #[snafu(transparent)]
278 ApiStorageError { source: ApiStorageError },
279}